1 ///
2 module fluid.number_input;
3 
4 import std.ascii;
5 import std.range;
6 import std.traits;
7 import std.algorithm;
8 
9 import fluid.node;
10 import fluid.input;
11 import fluid.utils;
12 import fluid.style;
13 import fluid.backend;
14 import fluid.structs;
15 import fluid.text_input;
16 
17 alias numberInput(T) = simpleConstructor!(NumberInput!T);
18 alias intInput = simpleConstructor!IntInput;
19 alias floatInput = simpleConstructor!FloatInput;
20 
21 alias IntInput = NumberInput!int;
22 alias FloatInput = NumberInput!float;
23 
24 alias numberInputSpinner = simpleConstructor!NumberInputSpinner;
25 
26 
27 @safe:
28 
29 /// Number input field.
30 class NumberInput(T) : AbstractNumberInput {
31 
32     static assert(isNumeric!T, "NumberInput is only compatible with numeric types.");
33 
34     mixin enableInputActions;
35 
36     public {
37 
38         /// Value of the input.
39         T value = 0;
40 
41         /// Step used by the increment/decrement button.
42         T step = 1;
43 
44         /// Minimum and maximum value for the input, inclusive on both ends.
45         static if (isFloatingPoint!T) {
46             T minValue = -T.infinity;
47             T maxValue = +T.infinity;
48         }
49         else {
50             T minValue = T.min;
51             T maxValue = T.max;
52         }
53 
54     }
55 
56     private {
57 
58         /// If true, the expression passed to the input has been modified. The value will be updated as soon as the
59         /// input is submitted or loses focus.
60         bool isDirty;
61 
62     }
63 
64     this(void delegate() @safe changed = null) {
65 
66         super(changed);
67 
68     }
69 
70     this(T value, void delegate() @safe changed = null) {
71 
72         super(changed);
73         this.value = value;
74         this.update();
75 
76     }
77 
78     override void drawImpl(Rectangle outer, Rectangle inner) {
79 
80         auto style = pickStyle();
81 
82         super.drawImpl(outer, inner);
83         spinner.draw(inner);
84 
85         // Re-evaluate the expression if focus was lost
86         if (!isFocused) evaluate();
87 
88     }
89 
90     /// Update the value.
91     protected void evaluate() {
92 
93         // Ignore if clean, no changes were made
94         if (!isDirty) return;
95 
96         // Evaluate the expression
97         evaluateImpl();
98 
99         // Update the text
100         update();
101 
102         // Call change callback
103         if (changed) changed();
104 
105     }
106 
107     private void evaluateImpl() {
108 
109         // TODO handle failure properly, add a warning sign or something, preserve old value
110         this.value = evaluateExpression!T(super.value).value.clamp(minValue, maxValue);
111 
112         // Mark as clean
113         isDirty = false;
114 
115     }
116 
117     private void update() {
118 
119         import std.conv;
120 
121         // Update the textual value
122         super.value = this.value.to!(char[]);
123 
124         // Move the caret
125         caretToEnd();
126 
127         // Resize
128         updateSize();
129 
130     }
131 
132     /// Increase the value by a step.
133     @(FluidInputAction.scrollUp)
134     override void increment() {
135 
136         evaluateImpl();
137         value += step;
138         update();
139         touch();
140         focus();
141 
142         // Call change callback
143         if (changed) changed();
144 
145     }
146 
147     /// Decrease the value by a step.
148     @(FluidInputAction.scrollDown)
149     override void decrement() {
150 
151         evaluateImpl();
152         value -= step;
153         update();
154         touch();
155         focus();
156 
157         // Call change callback
158         if (changed) changed();
159 
160     }
161 
162     override protected void touchText() {
163 
164         // Instead of calling the callback, simply mark the input as dirty
165         isDirty = true;
166 
167     }
168 
169     /// Submit the value.
170     @(FluidInputAction.submit)
171     override void submit() {
172 
173         // Evaluate the expression
174         evaluate();
175 
176         // Submit
177         super.submit();
178 
179     }
180 
181 }
182 
183 ///
184 unittest {
185 
186     // intInput lets the user specify any integer value
187     intInput();
188 
189     // Float input allows floating point values
190     floatInput();
191 
192     // Specify a callback to update other components as the value of this input changes
193     IntInput myInput;
194 
195     myInput = intInput(delegate {
196 
197         int result = myInput.value;
198 
199     });
200 
201 }
202 
203 unittest {
204 
205     int calls;
206 
207     auto io = new HeadlessBackend;
208     auto root = intInput(delegate {
209 
210         calls++;
211 
212     });
213 
214     root.io = io;
215 
216     // First frame: initial state
217     root.focus();
218     root.draw();
219 
220     assert(root.value == 0);
221     assert(root.TextInput.value == "0");
222 
223     // Second frame, type in "10"
224     io.nextFrame();
225     io.inputCharacter("10");
226     root.draw();
227 
228     // Value should remain unchanged
229     assert(calls == 0);
230     assert(root.value == 0);
231     assert(root.TextInput.value.among("010", "10"));
232 
233     // Hit enter to update
234     io.nextFrame;
235     io.press(KeyboardKey.enter);
236     root.draw();
237 
238     assert(calls == 1);
239     assert(root.value == 10);
240     assert(root.TextInput.value == "10");
241 
242     // Test math equations
243     io.nextFrame;
244     io.inputCharacter("+20*5");
245     io.release(KeyboardKey.enter);
246     root.focus();
247     root.draw();
248 
249     assert(calls == 1);
250     assert(root.value == 10);
251     assert(root.TextInput.value == "10+20*5");
252 
253     // Submit the expression
254     io.nextFrame;
255     io.press(KeyboardKey.enter);
256     root.draw();
257 
258     assert(calls == 2);
259     assert(root.value != (10+20)*5);
260     assert(root.value == 110);
261     assert(root.TextInput.value == "110");
262 
263     // Try incrementing
264     io.nextFrame;
265     io.mousePosition = start(root.spinner._lastRectangle);
266     io.press;
267     root.draw();
268 
269     io.nextFrame;
270     io.release;
271     root.draw();
272 
273     assert(calls == 3);
274     assert(root.value == 111);
275     assert(root.TextInput.value == "111");
276 
277     // Try decrementing
278     io.nextFrame;
279     io.mousePosition = end(root.spinner._lastRectangle) - Vector2(1, 1);
280     io.press;
281     root.draw();
282 
283     io.nextFrame;
284     io.release;
285     root.draw();
286 
287     assert(calls == 4);
288     assert(root.value == 110);
289     assert(root.TextInput.value == "110");
290 
291 }
292 
293 unittest {
294 
295     import std.math;
296 
297     auto io = new HeadlessBackend;
298     auto root = floatInput();
299 
300     root.io = io;
301 
302     io.inputCharacter("10e8");
303     root.focus();
304     root.draw();
305 
306     io.nextFrame;
307     io.press(KeyboardKey.enter);
308     root.draw();
309 
310     assert(root.value.isClose(10e8));
311     assert(root.TextInput.value.among("1e+9", "1e+09"));
312 
313 }
314 
315 abstract class AbstractNumberInput : TextInput {
316 
317     mixin enableInputActions;
318 
319     public {
320 
321         /// "Spinner" controlling the decrement and increment buttons.
322         NumberInputSpinner spinner;
323 
324     }
325 
326     this(void delegate() @safe changed = null) {
327 
328         super("");
329         super.changed = changed;
330         super.value = ['0'];
331         this.spinner = numberInputSpinner(.layout!"fill", &increment, &decrement);
332         caretToEnd();
333 
334     }
335 
336     override void resizeImpl(Vector2 space) {
337 
338         super.resizeImpl(space);
339         spinner.resize(tree, theme, space);
340 
341     }
342 
343     abstract void increment();
344     abstract void decrement();
345 
346 }
347 
348 /// Increment and decrement buttons that appear on the right of number inputs.
349 class NumberInputSpinner : Node, FluidHoverable {
350 
351     mixin enableInputActions;
352 
353     /// Additional features available for number input styling
354     static class Extra : typeof(super).Extra {
355 
356         /// Image to use for the increment/decrement buttons.
357         Image buttons;
358 
359         this(Image buttons) {
360 
361             this.buttons = buttons;
362 
363         }
364 
365     }
366 
367     public {
368 
369         void delegate() @safe incremented;
370         void delegate() @safe decremented;
371 
372     }
373 
374     private {
375 
376         Rectangle _lastRectangle;
377 
378     }
379 
380     this(void delegate() @safe incremented, void delegate() @safe decremented) {
381 
382         this.incremented = incremented;
383         this.decremented = decremented;
384 
385     }
386 
387     override ref inout(bool) isDisabled() inout {
388 
389         return super.isDisabled();
390 
391     }
392 
393     override bool isHovered() const {
394 
395         return super.isHovered();
396 
397     }
398 
399     override void resizeImpl(Vector2) {
400 
401         minSize = Vector2();
402 
403     }
404 
405     protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) {
406 
407         import fluid.utils : contains;
408 
409         return buttonsRectangle(style, rect).contains(mousePosition);
410 
411     }
412 
413     override void drawImpl(Rectangle outer, Rectangle inner) {
414 
415         auto style = pickStyle();
416 
417         style.drawBackground(io, outer);
418 
419         // If there's a texture for buttons, display it
420         if (auto texture = getTexture(style)) {
421 
422             _lastRectangle = buttonsRectangle(style, inner);
423 
424             texture.draw(_lastRectangle);
425 
426         }
427 
428     }
429 
430     /// Get rectangle for the buttons
431     Rectangle buttonsRectangle(const Style style, Rectangle inner) {
432 
433         if (auto texture = getTexture(style)) {
434 
435             const scale = inner.height / texture.height;
436             const size = Vector2(texture.width, texture.height) * scale;
437             const position = end(inner) - size;
438 
439             return Rectangle(position.tupleof, size.tupleof);
440 
441         }
442 
443         return Rectangle.init;
444 
445     }
446 
447     @(FluidInputAction.press)
448     void press() {
449 
450         // Above center (increment)
451         if (io.mousePosition.y < center(_lastRectangle).y) {
452 
453             if (incremented) incremented();
454 
455         }
456 
457         // Below center (decrement)
458         else {
459 
460             if (decremented) decremented();
461 
462         }
463 
464     }
465 
466     void mouseImpl() {
467 
468     }
469 
470     /// Get texture used by the spinner.
471     protected TextureGC* getTexture(const Style style) @trusted {
472 
473         auto extra = cast(Extra) style.extra;
474 
475         if (!extra) return null;
476 
477         // Check entries for this backend
478         return extra.getTexture(io, extra.buttons);
479 
480     }
481 
482 }
483 
484 struct ExpressionResult(T) {
485 
486     T value = 0;
487     bool success;
488 
489     alias value this;
490 
491     bool opCast(T : bool)() {
492 
493         return success;
494 
495     }
496 
497     ExpressionResult op(dchar operator, ExpressionResult rhs) {
498 
499         // Both sides must be successful
500         if (success && rhs.success)
501         switch (operator) {
502 
503             case '+':
504                 return ExpressionResult(value + rhs.value, true);
505             case '-':
506                 return ExpressionResult(value - rhs.value, true);
507             case '*':
508                 return ExpressionResult(value * rhs.value, true);
509             case '/':
510                 return ExpressionResult(value / rhs.value, true);
511             default: break;
512 
513         }
514 
515         // Failure
516         return ExpressionResult.init;
517 
518     }
519 
520     string toString() const {
521 
522         import std.conv;
523 
524         if (success)
525             return value.to!string;
526         else
527             return "failure";
528 
529     }
530 
531 }
532 
533 ExpressionResult!T evaluateExpression(T, Range)(Range input) {
534 
535     import std.utf : byDchar;
536 
537     alias Result = ExpressionResult!T;
538 
539     // Skip whitespace
540     auto expression = input.byDchar.filter!(a => !a.isWhite);
541 
542     return evaluateExpressionImpl!T(expression);
543 
544 }
545 
546 unittest {
547 
548     assert(evaluateExpression!int("0") == 0);
549     assert(evaluateExpression!int("10") == 10);
550     assert(evaluateExpression!int("123") == 123);
551     assert(evaluateExpression!int("-0") == -0);
552     assert(evaluateExpression!int("-10") == -10);
553     assert(evaluateExpression!int("-123") == -123);
554 
555     assert(evaluateExpression!int("2+2") == 4);
556     assert(evaluateExpression!int("2+2-3") == 1);
557     assert(evaluateExpression!int("1+1*10+3") == 14);
558     assert(evaluateExpression!int("1+2*10+3") == 24);
559     assert(evaluateExpression!int("10+-10") == 0);
560     assert(evaluateExpression!int("4*8") == 32);
561     assert(evaluateExpression!int("20/5") == 4);
562 
563     assert(evaluateExpression!int("3/4") == 0);
564     assert(evaluateExpression!float("3/4") == 0.75);
565 
566     assert(evaluateExpression!int("(4+5)*2") == 18);
567     assert(evaluateExpression!int("(4+5)+2*2") == 9+4);
568     assert(evaluateExpression!int("(4+4*5)*10+7") == (4+4*5)*10+7);
569     assert(evaluateExpression!int("102+(4+4*5)*10+7") == 102+(4+4*5)*10+7);
570 
571 }
572 
573 unittest {
574 
575     import std.math;
576     import std.conv;
577 
578     assert(evaluateExpression!float("2.0+2.0").isClose(4.0));
579     assert(evaluateExpression!float("2.4*4.2").to!string == "10.08");
580     assert(evaluateExpression!float("3/4").isClose(0.75));
581     assert(evaluateExpression!float("2 * 0.75").isClose(1.5));
582     assert(evaluateExpression!float("-2 * 0.75 * 100").isClose(-150));
583 
584     assert(evaluateExpression!float("2e8").isClose(2e8));
585     assert(evaluateExpression!float("-2e8").isClose(-2e8));
586     assert(evaluateExpression!float("2e+8").isClose(2e+8));
587     assert(evaluateExpression!float("2e-8").to!string == "2e-08");
588     assert(evaluateExpression!float("-2e+8").isClose(-2e+8));
589 
590 }
591 
592 private {
593 
594     ExpressionResult!T evaluateExpressionImpl(T, Range)(ref Range input, int minPrecedence = 1) {
595 
596         // Reference: https://eli.thegreenplace.net/2012/08/02/parsing-expressions-by-precedence-climbing
597 
598         // Evaluate the left-hand side
599         auto lhs = evaluateAtom!T(input);
600 
601         // Load binary operator chain
602         while (!input.empty) {
603 
604             const operator = input.front;
605             const precedence = .precedence(operator);
606             const nextMinPrecedence = precedence + 1;
607 
608             // Precedence too low
609             if (precedence < minPrecedence) break;
610 
611             input.popFront;
612 
613             auto rhs = evaluateExpressionImpl!T(input, nextMinPrecedence);
614 
615             lhs = lhs.op(operator, rhs);
616 
617         }
618 
619         return lhs;
620 
621     }
622 
623     int precedence(dchar operator) {
624 
625         if (operator.among('+', '-'))
626             return 1;
627 
628         else if (operator.among('*', '/'))
629             return 2;
630 
631         // Error
632         else return 0;
633 
634     }
635 
636     ExpressionResult!T evaluateAtom(T, Range)(ref Range expression) {
637 
638         bool negative;
639         ExpressionResult!T result;
640 
641         // Fail if there's nothing ahead
642         if (expression.empty) return result.init;
643 
644         // Negate the value
645         if (expression.front == '-') {
646 
647             negative = true;
648             expression.popFront;
649 
650         }
651 
652         // Found paren
653         if (expression.front == '(') {
654 
655             expression.popFront;
656 
657             // Load an expression
658             result = evaluateExpressionImpl!T(expression);
659 
660             // Expect it to end
661             if (expression.front != ')')
662                 return result.init;
663 
664             expression.popFront;
665 
666         }
667 
668         // Load the number
669         else {
670 
671             import std.conv;
672 
673             bool exponent;
674 
675             // Parsing floats is hard! We'll just locate the end of the number and use std.conv.to.
676             auto length = expression.countUntil!((a) {
677 
678                 // Allow 'e+' and 'e-'
679                 if (a == 'e') {
680                     exponent = true;
681                     return false;
682                 }
683                 if (exponent && a.among('+', '-')) {
684                     exponent = false;
685                     return false;
686                 }
687 
688                 // Take in digits and dots
689                 return !a.isDigit && a != '.';
690 
691             });
692 
693             // Parse the number
694             try result.value = expression.take(length).to!T;
695             catch (ConvException)
696                 return result.init;
697 
698             // Skip ahead
699             expression.popFrontN(length);
700 
701         }
702 
703         if (negative) {
704             result.value = -result.value;
705         }
706 
707         result.success = true;
708 
709         return result;
710 
711     }
712 
713 }