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