1 module fluid.password_input;
2 
3 import fluid.utils;
4 import fluid.backend;
5 import fluid.text_input;
6 
7 
8 @safe:
9 
10 
11 /// A password input box.
12 alias passwordInput = simpleConstructor!PasswordInput;
13 
14 /// ditto
15 class PasswordInput : TextInput {
16 
17     mixin enableInputActions;
18 
19     protected {
20 
21         /// Character circle radius.
22         float radius;
23 
24         /// Distance between the start positions of each character.
25         float advance;
26 
27     }
28 
29     private {
30 
31         char[][] _bufferHistory;
32 
33     }
34 
35     /// Create a password input.
36     /// Params:
37     ///     placeholder = Placeholder text for the field.
38     ///     submitted   = Callback for when the field is submitted.
39     this(string placeholder = "", void delegate() @trusted submitted = null) {
40 
41         super(placeholder, submitted);
42 
43     }
44 
45     /// PasswordInput does not support multiline.
46     override bool multiline() const {
47 
48         return false;
49 
50     }
51 
52     /// Delete all textual data created by the password box. All text typed inside the box will be overwritten, except
53     /// for any copies, if they were made. Clears the box.
54     ///
55     /// The password box keeps a buffer of all text that has ever been written to it, in order to store and display its
56     /// content. The security implication is that, even if the password is no longer needed, it will remain in program
57     /// memory, exposing it as a possible target for attackers, in case [memory corruption vulnerabilities][1] are
58     /// found. Even if collected by the garbage collector, the value will remain untouched until the same spot in memory
59     /// is reused, so in order to increase the security of a program, passwords should thus be *shredded* after usage,
60     /// explicitly overwriting their contents.
61     ///
62     /// Do note that shredding is never performed automatically — this function has to be called explicitly.
63     /// Furthermore, text provided through different means than explicit input or `push(Rope)` will not be cleared.
64     ///
65     /// [1]: https://en.wikipedia.org/wiki/Memory_safety
66     void shred() {
67 
68         // Go through each buffer
69         foreach (buffer; _bufferHistory) {
70 
71             // Clear it
72             buffer[] = char.init;
73 
74         }
75 
76         // Clear the input and buffer history
77         clear();
78         _bufferHistory = [buffer];
79 
80         // Clear undo stack
81         clearHistory();
82 
83     }
84 
85     unittest {
86 
87         import fluid.input;
88 
89         auto root = passwordInput();
90         root.value = "Hello, ";
91         root.caretToEnd();
92         root.push("World!");
93 
94         assert(root.value == "Hello, World!");
95 
96         auto value1 = root.value;
97         root.shred();
98 
99         assert(root.value == "");
100         assert(value1 == "Hello, \xFF\xFF\xFF\xFF\xFF\xFF");
101 
102         root.push("Hello, World!");
103         root.runInputAction!(FluidInputAction.previousChar);
104 
105         auto value2 = root.value;
106         root.chopWord();
107         root.push("Fluid");
108 
109         auto value3 = root.value;
110 
111         assert(root.value == "Hello, Fluid!");
112         assert(value2 == "Hello, World!");
113         assert(value3 == "Hello, Fluid!");
114 
115         root.shred();
116 
117         assert(root.value == "");
118         assert(value2 == "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
119         assert(value3 == value2);
120 
121     }
122 
123     unittest {
124 
125         auto root = passwordInput();
126         root.push("Hello, x");
127         root.chop();
128         root.push("World!");
129 
130         assert(root.value == "Hello, World!");
131 
132         root.undo();
133 
134         assert(root.value == "Hello, ");
135 
136         root.shred();
137 
138         assert(root.value == "");
139 
140         root.undo();
141 
142         assert(root.value == "");
143 
144         root.redo();
145         root.redo();
146 
147         assert(root.value == "");
148 
149     }
150 
151     protected override void drawContents(Rectangle inner, Rectangle innerScrolled) {
152 
153         auto typeface = pickStyle.getTypeface;
154 
155         // Empty, draw the placeholder using regular input
156         if (isEmpty) return super.drawContents(inner, innerScrolled);
157 
158         // Draw selection
159         drawSelection(innerScrolled);
160 
161         auto cursor = start(innerScrolled) + Vector2(radius, typeface.lineHeight / 2f);
162 
163         // Draw a circle for each character
164         foreach (_; value) {
165 
166             io.drawCircle(cursor, radius, style.textColor);
167 
168             cursor.x += advance;
169 
170         }
171 
172         // Draw the caret
173         drawCaret(innerScrolled);
174 
175     }
176 
177     override size_t nearestCharacter(Vector2 needle) const {
178 
179         import std.utf : byDchar;
180 
181         size_t number;
182 
183         foreach (ch; value[].byDchar) {
184 
185             // Stop if found the character
186             if (needle.x < number * advance + radius) break;
187 
188             number++;
189 
190         }
191 
192         return number;
193 
194     }
195 
196     protected override Vector2 caretPositionImpl(float availableWidth, bool preferNextLine) {
197 
198         return Vector2(
199             advance * valueBeforeCaret.countCharacters,
200             super.caretPositionImpl(availableWidth, preferNextLine).y,
201         );
202 
203     }
204 
205     /// Draw selection, if applicable.
206     protected override void drawSelection(Rectangle inner) {
207 
208         import std.range : enumerate;
209         import std.algorithm : min, max;
210 
211         // Ignore if selection is empty
212         if (selectionStart == selectionEnd) return;
213 
214         const typeface = style.getTypeface;
215 
216         const low = min(selectionStart, selectionEnd);
217         const high = max(selectionStart, selectionEnd);
218 
219         const start = advance * value[0 .. low].countCharacters;
220         const size = advance * value[low .. high].countCharacters;
221 
222         const rect = Rectangle(
223             (inner.start + Vector2(start, 0)).tupleof,
224             size, typeface.lineHeight,
225         );
226 
227         io.drawRectangle(rect, style.selectionBackgroundColor);
228 
229     }
230 
231     protected override void reloadStyles() {
232 
233         super.reloadStyles();
234 
235         // Use the "X" character as reference
236         auto typeface = style.getTypeface;
237         auto x = typeface.advance('X').x;
238 
239         radius = x / 2f;
240         advance = x * 1.2;
241 
242     }
243 
244     /// Request a new or larger buffer.
245     ///
246     /// `PasswordInput` keeps track of all the buffers that have been used since its creation in order to make it
247     /// possible to `shred` the contents once they're unnecessary.
248     ///
249     /// Params:
250     ///     minimumSize = Minimum size to allocate for the buffer.
251     protected override void newBuffer(size_t minimumSize = 64) {
252 
253         // Create the buffer
254         super.newBuffer(minimumSize);
255 
256         // Remember the buffer
257         _bufferHistory ~= buffer;
258 
259     }
260 
261 }
262 
263 ///
264 unittest {
265 
266     // PasswordInput lets you ask the user for passwords
267     auto node = passwordInput();
268 
269     // Retrieve the password with `value`
270     auto userPassword = node.value;
271 
272     // Destroy the passwords once you're done to secure them against attacks
273     // (Careful: This will invalidate `userPassword` we got earlier)
274     node.shred();
275 
276 }