1 module rpui.widgets.text_input.widget;
2 
3 import rpui.cursor;
4 import rpui.events;
5 import rpui.widget_events;
6 import rpui.input;
7 import rpui.math;
8 import rpui.primitives;
9 import rpui.widget;
10 import rpui.platform;
11 import rpui.widgets.text_input.renderer;
12 import rpui.widgets.text_input.edit_component;
13 import rpui.widgets.text_input.number_input_type_component;
14 
15 class TextInput : Widget {
16     enum InputType { text, integer, number }
17 
18     @field Align textAlign = Align.left;
19     @field bool autoSelectOnFocus = false;
20     @field float maxValue = float.max;
21     @field float minValue = float.min_normal;
22     @field float numberStep = 1f;
23     @field utf32string prefix = "";
24     @field utf32string postfix = "";
25     @field bool softPostfix = false;
26     @field bool displaySlider = true;
27 
28     private InputType inputType_ = InputType.text;
29 
30     @field
31     @property InputType inputType() { return inputType_; }
32     @property void inputType(in InputType val) {
33         inputType_ = val;
34 
35         if (val == InputType.integer || val == InputType.number) {
36             focusOnMousUp = true;
37             autoSelectOnFocus = true;
38         } else {
39             focusOnMousUp = false;
40         }
41     }
42 
43     @field
44     @property utf32string text() { return editComponent.text; }
45 
46     @property void text(in utf32string value) {
47         editComponent.text = value;
48     }
49 
50     @property utf32string selectedText() {
51         return editComponent.text[editComponent.selectRegion.start .. editComponent.selectRegion.end];
52     }
53 
54     package struct Measure {
55         float textWidth = 0;
56         float lineHeight = 0;
57         float textTopMargin;
58         float textLeftMargin;
59         float textRightMargin;
60         float prefixWidth = 0;
61         float postfixWidth = 0;
62         float carriageBoundary;
63         vec2 textRelativePosition = vec2(0);
64         float arrowsAreaWidth = 0;
65     }
66 
67     package Measure measure;
68     package EditComponent editComponent;
69     package NumberInputTypeComponent numberInputTypeComponent;
70 
71     this(in string style = "TextInput") {
72         super(style);
73         this.drawChildren = false;
74         this.renderer = new TextInputRenderer();
75         this.drawChildren = false;
76 
77         size = vec2(50, 21);
78     }
79 
80     override void onProgress(in ProgressEvent event) {
81         editComponent.carriage.onProgress(event);
82         updateCarriagePostion();
83         updateScroll();
84 
85         if (isNumberMode()) {
86         //     updateArrowAbsolutePositions();
87             numberInputTypeComponent.updateArrowStates();
88         }
89 
90         locator.updateAbsolutePosition();
91         locator.updateLocationAlign();
92         locator.updateVerticalLocationAlign();
93         locator.updateRegionAlign();
94 
95         renderer.onProgress(event);
96     }
97 
98     override void onCreate() {
99         super.onCreate();
100 
101         loadMeasure();
102         editComponent.carriage.attach(&editComponent);
103         numberInputTypeComponent.attach(this);
104 
105         if (isNumberMode()) {
106             textAlign = Align.center;
107         }
108 
109         events.subscribe!CopyCommand(&onCopy);
110         events.subscribe!CutCommand(&onCut);
111         events.subscribe!PasteCommand(&onPaste);
112         events.subscribe!UnselectCommand(&onUnselect);
113         events.subscribe!SelectAllCommand(&onSelectAll);
114     }
115 
116     private void onCopy(in CopyCommand command) {
117         if (!isFocused) {
118             return;
119         }
120 
121         platformSetClipboardTextUtf32(selectedText);
122         editComponent.unselect();
123     }
124 
125     private void onCut(in CutCommand command) {
126         if (!isFocused) {
127             return;
128         }
129 
130         if (selectedText.length == 0)
131             return;
132 
133         platformSetClipboardTextUtf32(selectedText);
134         editComponent.removeSelectedRegion();
135         events.notify(ChangeEvent());
136     }
137 
138     private void onPaste(in PasteCommand command) {
139         if (!isFocused) {
140             return;
141         }
142 
143         const text = platformGetClipboardTextUtf32();
144 
145         if (editComponent.enterText(text))
146             events.notify(ChangeEvent());
147     }
148 
149     private void onUnselect(in UnselectCommand command) {
150         if (!isFocused) {
151             return;
152         }
153 
154         editComponent.unselect();
155     }
156 
157     private void onSelectAll(in SelectAllCommand command) {
158         if (!isFocused) {
159             return;
160         }
161 
162         editComponent.selectAll();
163     }
164 
165     private void loadMeasure() {
166         with (view.theme.tree) {
167             measure.textLeftMargin = data.getNumber(style ~ ".textLeftMargin.0");
168             measure.textRightMargin = data.getNumber(style ~ ".textRightMargin.0");
169             measure.textTopMargin = data.getNumber(style ~ ".textTopMargin.0");
170             measure.carriageBoundary = data.getNumber(style ~ ".carriageBoundary.0");
171             measure.arrowsAreaWidth = data.getNumber(style ~ ".arrowsAreaWidth.0");
172         }
173     }
174 
175     override void updateSize() {
176         super.updateSize();
177         updateCarriagePostion();
178 
179         if (isNumberMode()) {
180             // updateArrowAbsolutePositions();
181             numberInputTypeComponent.updateArrowStates();
182         }
183     }
184 
185     override void onBlur(in BlurEvent event) {
186         editComponent.reset();
187         editComponent.onBlur();
188     }
189 
190     override void onFocus(in FocusEvent event) {
191         editComponent.onFocus();
192 
193         if (autoSelectOnFocus && !isFocused)
194             editComponent.selectAll();
195     }
196 
197     package bool isNumberMode() {
198         return inputType == InputType.integer || inputType == InputType.number;
199     }
200 
201     /// Change system cursor when mouse entering to arrows.
202     override void onCursor() {
203         if (isFocused && (isEnter || isClick)) {
204             view.cursor = CursorIcon.iBeam;
205         }
206 
207         if (numberInputTypeComponent.leftArrow.isEnter) {
208             view.cursor = CursorIcon.normal;
209         }
210         else if (numberInputTypeComponent.rightArrow.isEnter) {
211             view.cursor = CursorIcon.normal;
212         }
213     }
214 
215     private void updateScroll() {
216         if (textAlign == Align.center) {
217             editComponent.scrollDelta = 0;
218             return;
219         }
220 
221         const rightBorder = absolutePosition.x + size.x - measure.textRightMargin - measure.postfixWidth;
222         const leftBorder = absolutePosition.x + measure.textLeftMargin + measure.prefixWidth;
223         const padding = measure.textRightMargin + measure.textLeftMargin +
224             measure.postfixWidth + measure.prefixWidth;
225         const regionOffset = editComponent.getTextRegionSize(0, editComponent.carriage.pos);
226         const textSize = cast(float) measure.textWidth;
227         const minScroll = -textSize + size.x - padding;
228 
229         if (editComponent.scrollDelta <= minScroll)
230             editComponent.scrollDelta = minScroll;
231 
232         if (textSize + padding < size.x) {
233             editComponent.scrollDelta = 0;
234         }
235         else if (editComponent.carriage.absolutePosition.x > rightBorder) {
236             editComponent.scrollDelta = -regionOffset + size.x - padding;
237         }
238         else if (editComponent.carriage.absolutePosition.x < leftBorder) {
239             editComponent.scrollDelta = -regionOffset;
240         }
241 
242         updateCarriagePostion();
243     }
244 
245     private void updateCarriagePostion() {
246         editComponent.carriage.absolutePosition = absolutePosition +
247             editComponent.getTextRegionOffset(editComponent.carriage.pos);
248     }
249 
250 // Events ------------------------------------------------------------------------------------------
251 
252     override void onTextEntered(in TextEnteredEvent event) {
253         if (!isFocused)
254             return;
255 
256         if (editComponent.onTextEntered(event))
257             events.notify(ChangeEvent());
258     }
259 
260     override void onKeyPressed(in KeyPressedEvent event) {
261         if (!isFocused)
262             return;
263 
264         editComponent.onKeyPressed(event);
265     }
266 
267     override void onMouseDown(in MouseDownEvent event) {
268         if (isEnter) {
269             editComponent.onMouseDown(event);
270         }
271 
272         if (isNumberMode())
273             numberInputTypeComponent.onMouseDown(event);
274     }
275 
276     override void onMouseUp(in MouseUpEvent event) {
277         if (isNumberMode())
278             numberInputTypeComponent.onMouseUp(event);
279     }
280 
281     override void onMouseMove(in MouseMoveEvent event) {
282         if (isEnter)
283             editComponent.onMouseMove(event);
284 
285         if (isNumberMode())
286             numberInputTypeComponent.onMouseMove(event);
287     }
288 
289     override void onDblClick(in DblClickEvent event) {
290         if (isEnter)
291             editComponent.onDblClick(event);
292     }
293 
294     override void onTripleClick(in TripleClickEvent event) {
295         if (isEnter)
296             editComponent.onTripleClick(event);
297     }
298 }