1 module rpui.widgets.text_input.edit_component;
2 
3 import std.algorithm.comparison;
4 import std.algorithm.searching;
5 import std.string;
6 import std.math;
7 import std.conv;
8 
9 import rpui.input;
10 import rpui.primitives;
11 import rpui.math;
12 import rpui.widgets.text_input.widget;
13 import rpui.widgets.text_input.select_component;
14 import rpui.widgets.text_input.carriage;
15 import rpui.widgets.text_input.transforms_system;
16 import rpui.events;
17 import rpui.theme;
18 
19 struct EditComponent {
20     const commonSplitChars = " ,.;:?'!|/\\~*+-=(){}<>[]#%&^@$№`\""d;
21     const japanesePunctuation = "\u3000{}()[]【】、,…‥。〽「」『』〝〟〜:!?"d;
22     const splitChars = commonSplitChars ~ japanesePunctuation;
23 
24     utf32string text;
25     Carriage carriage;
26     float scrollDelta = 0.0f;
27     vec2 absoulteTextPosition;
28 
29     SelectRegion selectRegion;
30     TextInput textInput;
31     TextInputTransformsSystem transformsSystem;
32     private utf32string lastText;
33 
34     void attach(TextInput textInput, TextInputTransformsSystem transformsSystem) {
35         this.textInput = textInput;
36         this.transformsSystem = transformsSystem;
37     }
38 
39     void reset() {
40         selectRegion.stopSelection();
41         carriage.reset();
42     }
43 
44     void onKeyPressed(in KeyPressedEvent event) {
45         carriage.timer = 0;
46         carriage.visible = true;
47 
48         if (isKeyPressed(KeyCode.Shift) && !selectRegion.startedSelection)
49             selectRegion.startSelection(carriage.pos);
50 
51         switch (event.key) {
52             case KeyCode.Left:
53                 if (isKeyPressed(KeyCode.Ctrl)) {
54                     carriage.setCarriagePos(carriage.navigateCarriage(-1));
55                 } else {
56                     carriage.moveCarriage(-1);
57                 }
58 
59                 break;
60 
61             case KeyCode.Right:
62                 if (isKeyPressed(KeyCode.Ctrl)) {
63                     carriage.setCarriagePos(carriage.navigateCarriage(1));
64                 } else {
65                     carriage.moveCarriage(1);
66                 }
67 
68                 break;
69 
70             case KeyCode.Home:
71                 carriage.setCarriagePos(0);
72                 break;
73 
74             case KeyCode.End:
75                 carriage.setCarriagePos(cast(int) text.length);
76                 break;
77 
78             case KeyCode.Delete:
79                 if (selectRegion.textIsSelected()) {
80                     removeSelectedRegion();
81                 } else {
82                     if (isKeyPressed(KeyCode.Ctrl)) {
83                         const end = carriage.navigateCarriage(1);
84                         removeRegion(carriage.pos, end);
85                     } else {
86                         removeRegion(carriage.pos, carriage.pos+1);
87                     }
88                 }
89                 break;
90 
91             case KeyCode.BackSpace:
92                 if (selectRegion.textIsSelected()) {
93                     removeSelectedRegion();
94                 } else {
95                     if (isKeyPressed(KeyCode.Ctrl)) {
96                         const start = carriage.navigateCarriage(-1);
97                         removeRegion(start, carriage.pos);
98                     } else {
99                         removeRegion(carriage.pos-1, carriage.pos);
100                     }
101                 }
102                 break;
103 
104             default:
105                 // Nothing
106         }
107     }
108 
109     void removeSelectedRegion() {
110         removeRegion(
111             selectRegion.start,
112             selectRegion.end
113         );
114     }
115 
116     void removeRegion(in int start, in int end) {
117         if (start < 0 || end > text.length)
118             return;
119 
120         const leftPart = text[0 .. start];
121         const rightPart = text[end .. text.length];
122 
123         text = leftPart ~ rightPart;
124         carriage.setCarriagePos(start);
125     }
126 
127     private bool isISOControlCharacter(in utf32char ch) {
128         // Control characters
129         // https://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=0x
130         return (ch >= 0x00 && ch <= 0x1F) || (ch >= 0x7F && ch <= 0x9F);
131     }
132 
133     bool onTextEntered(in TextEnteredEvent event) {
134         return enterText(to!utf32string(event.key));
135     }
136 
137     bool enterText(in utf32string charToPut) {
138         carriage.timer = 0;
139         carriage.visible = true;
140 
141         foreach (ch; charToPut) {
142             if (isISOControlCharacter(ch))
143                 return false;
144         }
145 
146         // Splitting text to two parts by carriage position
147 
148         utf32string leftPart;
149         utf32string rightPart;
150         auto newCarriagePos = carriage.pos;
151 
152         if (!selectRegion.textIsSelected()) {
153             leftPart = text[0 .. carriage.pos];
154             rightPart = text[carriage.pos .. $];
155             newCarriagePos += charToPut.length;
156         }
157         else {
158             leftPart = text[0 .. selectRegion.start];
159             rightPart = text[selectRegion.end .. $];
160             newCarriagePos = selectRegion.start + cast(int) charToPut.length;
161         }
162 
163         const newText = leftPart ~ charToPut ~ rightPart;
164 
165         text = newText;
166         carriage.pos = newCarriagePos;
167 
168         selectRegion.stopSelection();
169         return true;
170     }
171 
172     void onMouseDown(in MouseDownEvent event) {
173         if (textInput.autoSelectOnFocus)
174             return;
175 
176         carriage.setCarriagePosFromMousePos(event.x, event.y);
177 
178         if (!isKeyPressed(KeyCode.Shift))
179             selectRegion.startSelection(carriage.pos);
180     }
181 
182     void onMouseMove(in MouseMoveEvent event) {
183         if (event.button != MouseButton.mouseLeft)
184             return;
185 
186         // if (!textInput.isNumberMode())
187         if (textInput.isFocused)
188             carriage.setCarriagePosFromMousePos(event.x, event.y);
189     }
190 
191     void onDblClick(in DblClickEvent event) {
192         const left = carriage.navigateCarriage(-1);
193         const right = carriage.navigateCarriage(1);
194 
195         selectRegion.start = left;
196         selectRegion.end = right;
197         carriage.pos = right;
198     }
199 
200     void onTripleClick(in TripleClickEvent event) {
201         selectAll();
202     }
203 
204     float getTextWidth() {
205         return textInput.measure.textWidth;
206     }
207 
208     float getTextRegionSize(in int start, in int end)
209         // in(start <= end)
210     {
211         if (start == end)
212             return 0.0f;
213 
214         return transformsSystem.getRegionTextWidth(start, end);
215     }
216 
217     vec2 getTextRegionOffset(in int charPos) {
218         const regionSize = getTextRegionSize(0, charPos);
219         const offset = vec2(
220             regionSize + scrollDelta,
221             textInput.measure.textTopMargin
222         );
223 
224         float alignOffset = 0;
225 
226         if (textInput.textAlign == Align.left) {
227             alignOffset = textInput.measure.textLeftMargin;
228         }
229         else if (textInput.textAlign == Align.right) {
230             alignOffset = -textInput.measure.textRightMargin;
231         }
232 
233         return vec2(alignOffset - textInput.measure.carriageBoundary, 0) +
234             textInput.measure.textRelativePosition + offset;
235     }
236 
237     void selectAll() {
238         selectRegion.start = 0;
239         selectRegion.end = cast(int) text.length;
240         selectRegion.startedSelection = true;
241         carriage.pos = selectRegion.end;
242     }
243 
244     void unselect() {
245         selectRegion.start = 0;
246         selectRegion.end = 0;
247         selectRegion.startedSelection = false;
248     }
249 
250     void onFocus() {
251         lastText = textInput.text;
252     }
253 
254     void onBlur() {
255         switch (textInput.inputType) {
256             case TextInput.InputType.integer:
257                 if (!isNumeric(textInput.text)) {
258                     textInput.text = lastText;
259                 } else {
260                     const value = textInput.text.to!float;
261                     textInput.text = round(value).to!utf32string;
262                 }
263 
264                 break;
265 
266             case TextInput.InputType.number:
267                 if (!isNumeric(textInput.text))
268                     textInput.text = lastText;
269 
270                 break;
271 
272             case TextInput.InputType.text:
273                 return;
274 
275             default:
276                 return;
277         }
278     }
279 }